typescript namespace嵌套引起的编译错误

记录一次遇到typescript使用namespace嵌套引起编译错误,以及如何解决

项目背景

项目使用gRPC作为前后端通信,所以会有后端生成的接口类型文件在前端项目中,其中很多类型结构长这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export namespace RuntimeStatus {
export namespace Progress {
export interface Quantity {
total?: number;
completed?: number;
unit?: string;
}
// export const a = '1';
}

export enum Severity {
UNSPECIFIED = 0,
NOTICE = 1,
WARNING = 2,
ERROR = 3,
CRITICAL = 4,
}
}

问题描述

在webpack@4 使用Babel编译时,它可以编译成功,但是由于项目决定升级到webpack@5,升级之后就报错:

1
2
3
4
5
6
7
8
9
10
11
Module parse failed: 'import' and 'export' may only appear at the top level (5:2)
File was processed with these loaders:
* ./node_modules/@pmmmwh/react-refresh-webpack-plugin/loader/index.js
* ./node_modules/babel-loader/lib/index.js
* ./node_modules/source-map-loader/dist/cjs.js
You may need an additional loader to handle the result of these loaders.
| export let RuntimeStatus;
| (function (_RuntimeStatus) {
> export let Progress;
| let Severity = /*#__PURE__*/function (Severity) {
| Severity[Severity["UNSPECIFIED"] = 0] = "UNSPECIFIED";

问题分析

我们可以看到报错显示function内部有一句export xxx, 这是报错的原因,于是我很纳闷,在怀疑是不是升级过程中插件版本不兼容导致的。

于是我重新用create-react-app@5生成一个新的项目,把这个代码片段插入进去,果然也是同样的报错信息,
但也同时实现了一个最小复现片段。

Typescript语法本身是支持这样的namespace嵌套的。
于是我就去Typescript官网的playground输入这段类型,
官网给出编译后的结果是:

1
2
3
4
5
6
7
8
9
10
11
var RuntimeStatus;
(function (RuntimeStatus) {
let Severity;
(function (Severity) {
Severity[Severity["UNSPECIFIED"] = 0] = "UNSPECIFIED";
Severity[Severity["NOTICE"] = 1] = "NOTICE";
Severity[Severity["WARNING"] = 2] = "WARNING";
Severity[Severity["ERROR"] = 3] = "ERROR";
Severity[Severity["CRITICAL"] = 4] = "CRITICAL";
})(Severity = RuntimeStatus.Severity || (RuntimeStatus.Severity = {}));
})(RuntimeStatus || (exports.RuntimeStatus = RuntimeStatus = {}));

官网编译成功且正确,于是我在typescript的github的issue中提了这个问题,他们回复这并不是typescript的问题,让我去问Babel问下,于是我又再Babel
的issue中提问。

作者很快答复了这确实Babel的插件Babel-plugin-transform-typescript编译的bug。

作者也给出了bug的原因:
在解析嵌套语句时,Babel会去解析嵌套语句的AST语法结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const transformed = handleNested(
path,
subNode.declaration,
t.identifier(name),
);
if (transformed !== null) {
isEmpty = false;
const moduleName = subNode.declaration.id.name;
if (names.has(moduleName)) {
namespaceTopLevel[i] = transformed;
} else {
names.add(moduleName);
namespaceTopLevel.splice(
i++,
1,
getDeclaration(moduleName),
transformed,
);
}
}

解析subNode的子节点,如果子节点只包含类型定义,比如interface, 也就是我们上面那段代码,那么解析结果就为null, 以上我们类型代码内部嵌套的namespace Progress并不包含值类型的定义,如果去掉注释中的export const a = '1'transformed就不是null
但对于解析结果为null的情况,需要删除这个AST节点,这样就不会有export let Progress这句了。Babel却忽略了这种情况。

解决问题

于是我加上了fix代码:

1
2
3
4
5
6
7
8
9
10
11
const transformed = handleNested(
path,
subNode.declaration,
t.identifier(name),
);
if (transformed !== null) {
...
} else {
namespaceTopLevel.splice(i, 1);
i--;
}

并验证编译结果:

1
2
3
4
5
6
7
8
9
10
11
12
export let RuntimeStatus;
(function (_RuntimeStatus) {
let Severity = /*#__PURE__*/function (Severity) {
Severity[Severity["UNSPECIFIED"] = 0] = "UNSPECIFIED";
Severity[Severity["NOTICE"] = 1] = "NOTICE";
Severity[Severity["WARNING"] = 2] = "WARNING";
Severity[Severity["ERROR"] = 3] = "ERROR";
Severity[Severity["CRITICAL"] = 4] = "CRITICAL";
return Severity;
}({});
_RuntimeStatus.Severity = Severity;
})(RuntimeStatus || (RuntimeStatus = {}));

可以看到function内部已经没有export了,说明对于transformednull的AST并不会被generated

我们再看下加上const语句的编译结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export let RuntimeStatus;
(function (_RuntimeStatus) {
let Progress;
(function (_Progress) {
const a = _Progress.a = '1';
})(Progress || (Progress = _RuntimeStatus.Progress || (_RuntimeStatus.Progress = {})));
let Severity = /*#__PURE__*/function (Severity) {
Severity[Severity["UNSPECIFIED"] = 0] = "UNSPECIFIED";
Severity[Severity["NOTICE"] = 1] = "NOTICE";
Severity[Severity["WARNING"] = 2] = "WARNING";
Severity[Severity["ERROR"] = 3] = "ERROR";
Severity[Severity["CRITICAL"] = 4] = "CRITICAL";
return Severity;
}({});
_RuntimeStatus.Severity = Severity;
})(RuntimeStatus || (RuntimeStatus = {}));

可以看到Progress中只有const语句被generated了,而不包含interface Quantity

结果

提交PR, contributor review and merge.